// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <string>
#include <cstdlib>
#include <securec.h>
#include <map>
#include "android/native_activity.h"
#include "android/native_window_jni.h"
#include "android/log.h"
#include "android/bitmap.h"
#include "CasJniBridge.h"
#include "CasLog.h"
#include "CasStreamRecvParser.h"
#include "CasController.h"

using namespace std;

ANativeWindow *gANativeWindow;
CasController *gJniApiCtrl = CasController::GetInstance();

// 用于jni回调java
static JavaVM *g_JVM = nullptr;
static jobject jniCallback = nullptr;
void *invokeCmdCallBack(int type, string msg);

static std::string jstring2string(JNIEnv *env, jstring jStr)
{
    if (!jStr) {
        return "";
    }
    const jclass stringClass = env->GetObjectClass(jStr);
    const jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
    const jbyteArray stringJbytes = (jbyteArray)env->CallObjectMethod(jStr, getBytes, env->NewStringUTF("UTF-8"));
    int length = env->GetArrayLength(stringJbytes);
    jbyte *pBytes = env->GetByteArrayElements(stringJbytes, nullptr);
    std::string ret = std::string((char *)pBytes, length);
    env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT);
    env->DeleteLocalRef(stringJbytes);
    env->DeleteLocalRef(stringClass);
    return ret;
}

JNIEXPORT jboolean JNICALL JNI(start)(JNIEnv *env, jclass, jobject surface, jboolean isHome)
{
    if (surface == nullptr) {
        ERR("Native window is null");
        return JNI_FALSE;
    }
    gANativeWindow = ANativeWindow_fromSurface(env, surface);

    if (gJniApiCtrl->Start(gANativeWindow, isHome)) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

#if defined(RECONNECT)

JNIEXPORT jboolean JNICALL JNI(reconnect)(JNIEnv *env, jclass)
{
    if (gJniApiCtrl->Reconnect()) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

#endif

JNIEXPORT void JNICALL JNI(stop)(JNIEnv *, jclass, jboolean isHome)
{
    gJniApiCtrl->Stop(isHome);
    if (!isHome && gANativeWindow != nullptr) {
        ANativeWindow_release(gANativeWindow);
        gANativeWindow = nullptr;
    }
}

JNIEXPORT jboolean JNICALL JNI(setMediaConfig)(JNIEnv *env, jclass, jobject mediaConfig)
{
    jclass hashMapClass = env->FindClass("java/util/HashMap");
    jmethodID entrySetMID = env->GetMethodID(hashMapClass, "entrySet", "()Ljava/util/Set;");

    jobject entrySetObj = env->CallObjectMethod(mediaConfig, entrySetMID);
    jclass entrySetClass = env->FindClass("java/util/Set");
    jmethodID iteratorMID = env->GetMethodID(entrySetClass, "iterator", "()Ljava/util/Iterator;");
    jobject iteratorObj = env->CallObjectMethod(entrySetObj, iteratorMID);

    jclass iteratorClass = env->FindClass("java/util/Iterator");
    jmethodID hasNextMID = env->GetMethodID(iteratorClass, "hasNext", "()Z");
    jmethodID nextMID = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");

    jclass entryClass = env->FindClass("java/util/Map$Entry");
    jmethodID getKeyMID = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
    jmethodID getValueMID = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");

    map<string, string> mediaConfigMap;

    while (env->CallBooleanMethod(iteratorObj, hasNextMID)) {
        jobject entryObj = env->CallObjectMethod(iteratorObj, nextMID);

        jstring mediaConfigKey = (jstring)env->CallObjectMethod(entryObj, getKeyMID);
        if (mediaConfigKey == nullptr) {
            continue;
        }
        string mediaConfigKeyStr = env->GetStringUTFChars(mediaConfigKey, nullptr);

        jstring mediaConfigValue = (jstring)env->CallObjectMethod(entryObj, getValueMID);
        if (mediaConfigValue == nullptr) {
            continue;
        }
        string mediaConfigValueStr = env->GetStringUTFChars(mediaConfigValue, nullptr);

        mediaConfigMap.insert(pair<string, string>(mediaConfigKeyStr, mediaConfigValueStr));
    }

    if (gJniApiCtrl->SetMediaConfig(mediaConfigMap)) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

JNIEXPORT void JNICALL JNI(setJniConf)(JNIEnv *env, jclass, jstring jKey, jstring jValue)
{
    std::string key = jstring2string(env, jKey);
    std::string value = jstring2string(env, jValue);
    gJniApiCtrl->SetJniConf(key, value);
}

JNIEXPORT jboolean JNICALL JNI(getConnectStatus)(JNIEnv *env, jclass)
{
    if (gJniApiCtrl->GetConnectStatus()) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

int gRotation;
int _gRotation;

void doRotateSync()
{
    _gRotation = gRotation;
}

extern "C" JNIEXPORT jboolean JNICALL JNICALL JNI(setRotation)(JNIEnv *env, jclass, jint rotation)
{
    doRotateSync();
    gRotation = rotation;
    return JNI_TRUE;
}

extern "C" JNIEXPORT jint JNICALL JNI(recvData)(JNIEnv *env, jclass, jbyte type, jbyteArray jData, jint length)
{
    uint8_t *data = (uint8_t *)env->GetByteArrayElements(jData, nullptr);
    int ret = gJniApiCtrl->JniRecvData((CasMsgType)type, data, length);
    env->ReleaseByteArrayElements(jData, (jbyte *)data, JNI_ABORT);
    return ret;
}

extern "C" JNIEXPORT jint JNICALL JNI(sendData)(JNIEnv *env, jclass clazz, jbyte type, jbyteArray jData, jint length) {
    uint8_t *data = (uint8_t *)env->GetByteArrayElements(jData, nullptr);
    int ret = gJniApiCtrl->JniSendData((CasMsgType)type, data, length);
    env->ReleaseByteArrayElements(jData, (jbyte *)data, JNI_ABORT);
    return ret;
}

extern "C" JNIEXPORT jboolean JNICALL JNI(sendTouchEvent)(JNIEnv *env, jclass, jint id, jint action, jint x, jint y,
    jint pressure, jlong time, jint orientation, jint height, jint width)
{
    if (gJniApiCtrl->SendTouchEvent(id, action, x, y, pressure, time, orientation, height, width)) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

extern "C" JNIEXPORT jboolean JNICALL JNI(sendKeyEvent)(JNIEnv *env, jclass, jint keycode, jint action)
{
    if (gJniApiCtrl->SendKeyEvent(keycode, action)) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

extern "C" JNIEXPORT jboolean JNICALL JNI(sendMotionEvent)(JNIEnv *env, jclass, jint masterAxis,
    jint masterValue, jint secondaryAxis, jint secondaryValue)
{
    if (gJniApiCtrl->SendMotionEvent(masterAxis, masterValue, secondaryAxis, secondaryValue)) {
        return JNI_TRUE;
    }
    return JNI_FALSE;
}

extern "C" JNIEXPORT jint JNICALL JNI(getJniStatus)(JNIEnv *env, jclass)
{
    int statusCode = gJniApiCtrl->GetState();
    return statusCode;
}

extern "C" JNIEXPORT jint JNICALL JNI(getLag)(JNIEnv *env, jclass)
{
    int lag = static_cast<int>(gJniApiCtrl->GetLag() / 1000);
    return lag;
}

extern "C" JNIEXPORT jstring JNICALL JNI(getVideoStreamStats)(JNIEnv *env, jclass)
{
    string statsString = gJniApiCtrl->GetVideoRecvStats();
    return env->NewStringUTF(statsString.c_str());
}

extern "C" JNIEXPORT jstring JNICALL JNI(getSimpleStreamStats)(JNIEnv *env, jclass)
{
    string statsString = gJniApiCtrl->GetSimpleRecvStats();
    return env->NewStringUTF(statsString.c_str());
}

extern "C" JNIEXPORT void JNICALL JNI(setAssetsData)(JNIEnv *env, jclass, jstring jFilename, jbyteArray jData,
    jint length)
{
    const char *filename = env->GetStringUTFChars(jFilename, 0);
    jbyte *data = env->GetByteArrayElements(jData, nullptr);

    env->ReleaseStringUTFChars(jFilename, filename);
    env->ReleaseByteArrayElements(jData, data, 0);
    env->ReleaseByteArrayElements(jData, data, 0);
}

extern "C" JNIEXPORT void JNICALL JNI(registerCasJNICallback)(JNIEnv *env, jclass, jobject callback)
{
    env->GetJavaVM(&g_JVM);
    jniCallback = env->NewGlobalRef(callback);
    gJniApiCtrl->SetCmdCallBackMethod(invokeCmdCallBack);
}

void *invokeCmdCallBack(int type, string msg)
{
    JNIEnv *env = nullptr;
    int mNeedDetach = 0;
    int getEnvStat = g_JVM->GetEnv((void **)&env, JNI_VERSION_1_4);
    INFO("Get env stat %d type %d.", getEnvStat, type);
    if (getEnvStat == JNI_EDETACHED) {
        if (g_JVM->AttachCurrentThread(&env, nullptr) != 0) {
            ERR("Attach current thread failed.");
            return nullptr;
        }
        mNeedDetach = JNI_TRUE;
    }
    jclass javaClass = env->GetObjectClass(jniCallback);
    if (javaClass == 0) {
        ERR("Unable to find class");
        g_JVM->DetachCurrentThread();
        return nullptr;
    }

    jmethodID javaCallbackId = env->GetMethodID(javaClass, "onCmdReceive", "(ILjava/lang/String;)V");
    if (javaCallbackId == nullptr) {
        ERR("Unable to find method.");
        return nullptr;
    }
    env->CallVoidMethod(jniCallback, javaCallbackId, type, env->NewStringUTF(msg.c_str()));
    INFO("Invoke cmd callback end.");
    if (mNeedDetach) {
        g_JVM->DetachCurrentThread();
    }
    env = nullptr;
    return nullptr;
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    JNIEnv *env = nullptr;
    jint result = -1;

    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    result = JNI_VERSION_1_6;
    g_JVM = jvm;
    INFO("JNI_OnLoad");
    return result;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    INFO("JNI_OnUnload");
}
